package org.newdawn.slick;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Cursor;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.Drawable;
import org.lwjgl.opengl.Pbuffer;
import org.lwjgl.opengl.PixelFormat;
import org.newdawn.slick.gui.GUIContext;
import org.newdawn.slick.openal.SoundStore;
import org.newdawn.slick.opengl.CursorLoader;
import org.newdawn.slick.opengl.ImageData;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;

import java.io.IOException;
import java.util.Properties;

/**
 * A generic game container that handles the game loop, fps recording and
 * managing the input system
 *
 * @author kevin
 */
public abstract class GameContainer implements GUIContext {
    /**
     * The renderer to use for all GL operations
     */
    protected static SGL GL = Renderer.get();
    /**
     * The shared drawable if any
     */
    protected static Drawable SHARED_DRAWABLE;
    /**
     * True if we require stencil bits
     */
    protected static boolean stencil;
    /**
     * The time the last frame was rendered
     */
    protected long lastFrame;
    /**
     * The last time the FPS recorded
     */
    protected long lastFPS;
    /**
     * The last recorded FPS
     */
    protected int recordedFPS;
    /**
     * The current count of FPS
     */
    protected int fps;
    /**
     * True if we're currently running the game loop
     */
    protected boolean running = true;
    /**
     * The width of the display
     */
    protected int width;
    /**
     * The height of the display
     */
    protected int height;
    /**
     * The game being managed
     */
    protected Game game;
    /**
     * The input system to pass to the game
     */
    protected Input input;
    /**
     * The FPS we want to lock to
     */
    protected int targetFPS = -1;
    /**
     * The minimum logic update interval
     */
    protected long minimumLogicInterval = 1;
    /**
     * The stored delta
     */
    protected long storedDelta;
    /**
     * The maximum logic update interval
     */
    protected long maximumLogicInterval = 0;
    /**
     * The last game started
     */
    protected Game lastGame;
    /**
     * True if we should clear the screen each frame
     */
    protected boolean clearEachFrame = true;
    /**
     * True if the game is paused
     */
    protected boolean paused;
    /**
     * True if we should force exit
     */
    protected boolean forceExit = true;
    /**
     * True if vsync has been requested
     */
    protected boolean vsync;
    /**
     * Smoothed deltas requested
     */
    protected boolean smoothDeltas;
    /**
     * The number of samples we'll attempt through hardware
     */
    protected int samples;
    /**
     * True if this context supports multisample
     */
    protected boolean supportsMultiSample;
    /**
     * True if we should render when not focused
     */
    protected boolean alwaysRender;
    /**
     * The default font to use in the graphics context
     */
    private Font defaultFont;
    /**
     * The graphics context to be passed to the game
     */
    private Graphics graphics;
    /**
     * True if we should show the fps
     */
    private boolean showFPS = true;

    /**
     * Create a new game container wrapping a given game
     *
     * @param game The game to be wrapped
     */
    protected GameContainer(Game game) {
        this.game = game;
        lastFrame = getTime();

        getBuildVersion();
        Log.checkVerboseLogSetting();
    }

    public static void enableStencil() {
        stencil = true;
    }

    /**
     * Enable shared OpenGL context. After calling this all containers created
     * will shared a single parent context
     *
     * @throws SlickException Indicates a failure to create the shared drawable
     */
    public static void enableSharedContext() throws SlickException {
        try {
            SHARED_DRAWABLE = new Pbuffer(64, 64, new PixelFormat(8, 0, 0), null);
        } catch (LWJGLException e) {
            throw new SlickException(
                "Unable to create the pbuffer used for shard context, buffers not supported", e);
        }
    }

    /**
     * Get the context shared by all containers
     *
     * @return The context shared by all the containers or null if shared context isn't enabled
     */
    public static Drawable getSharedContext() {
        return SHARED_DRAWABLE;
    }

    /**
     * Get the build number of slick
     *
     * @return The build number of slick
     */
    public static int getBuildVersion() {
        try {
            Properties props = new Properties();
            props.load(ResourceLoader.getResourceAsStream("version"));

            int build = Integer.parseInt(props.getProperty("build"));
            Log.info("Slick Build #" + build);

            return build;
        } catch (Exception e) {
            Log.info("Unable to determine Slick build number");
            return -1;
        }
    }

    /**
     * Indicate whether we want to try to use fullscreen multisampling. This will
     * give antialiasing across the whole scene using a hardware feature.
     *
     * @param samples The number of samples to attempt (2 is safe)
     */
    public void setMultiSample(int samples) {
        this.samples = samples;
    }

    /**
     * Check if this hardware can support multi-sampling
     *
     * @return True if the hardware supports multi-sampling
     */
    public boolean supportsMultiSample() {
        return supportsMultiSample;
    }

    /**
     * The number of samples we're attempting to performing using
     * hardware multisampling
     *
     * @return The number of samples requested
     */
    public int getSamples() {
        return samples;
    }

    /**
     * Indicate if we should force exitting the VM at the end
     * of the game (default = true)
     *
     * @param forceExit True if we should force the VM exit
     */
    public void setForceExit(boolean forceExit) {
        this.forceExit = forceExit;
    }

    /**
     * Indicate if we want to smooth deltas. This feature will report
     * a delta based on the FPS not the time passed. This works well with
     * vsync.
     *
     * @param smoothDeltas True if we should report smooth deltas
     */
    public void setSmoothDeltas(boolean smoothDeltas) {
        this.smoothDeltas = smoothDeltas;
    }

    /**
     * Check if the display is in fullscreen mode
     *
     * @return True if the display is in fullscreen mode
     */
    public boolean isFullscreen() {
        return false;
    }

    /**
     * Indicate whether we want to be in fullscreen mode. Note that the current
     * display mode must be valid as a fullscreen mode for this to work
     *
     * @param fullscreen True if we want to be in fullscreen mode
     * @throws SlickException Indicates we failed to change the display mode
     */
    public void setFullscreen(boolean fullscreen) throws SlickException {
    }

    /**
     * Get the aspect ratio of the screen
     *
     * @return The aspect ratio of the display
     */
    public float getAspectRatio() {
        return getWidth() / getHeight();
    }

    /**
     * Indicate if we should clear the screen at the beginning of each frame. If you're
     * rendering to the whole screen each frame then setting this to false can give
     * some performance improvements
     *
     * @param clear True if the the screen should be cleared each frame
     */
    public void setClearEachFrame(boolean clear) {
        this.clearEachFrame = clear;
    }

    /**
     * Renitialise the game and the context in which it's being rendered
     *
     * @throws SlickException Indicates a failure rerun initialisation routines
     */
    public void reinit() throws SlickException {
    }

    /**
     * Pause the game - i.e. suspend updates
     */
    public void pause() {
        setPaused(true);
    }

    /**
     * Resumt the game - i.e. continue updates
     */
    public void resume() {
        setPaused(false);
    }

    /**
     * Check if the container is currently paused.
     *
     * @return True if the container is paused
     */
    public boolean isPaused() {
        return paused;
    }

    /**
     * Indicates if the game should be paused, i.e. if updates
     * should be propogated through to the game.
     *
     * @param paused True if the game should be paused
     */
    public void setPaused(boolean paused) {
        this.paused = paused;
    }

    /**
     * True if this container should render when it has focus
     *
     * @return True if this container should render when it has focus
     */
    public boolean getAlwaysRender() {
        return alwaysRender;
    }

    /**
     * Indicate whether we want this container to render when it has focus
     *
     * @param alwaysRender True if this container should render when it has focus
     */
    public void setAlwaysRender(boolean alwaysRender) {
        this.alwaysRender = alwaysRender;
    }

    /**
     * Get the default system font
     *
     * @return The default system font
     */
    @Override
    public Font getDefaultFont() {
        return defaultFont;
    }

    /**
     * Set the default font that will be intialised in the graphics held in this container
     *
     * @param font The font to use as default
     */
    public void setDefaultFont(Font font) {
        if (font != null) {
            this.defaultFont = font;
        } else {
            Log.warn("Please provide a non null font");
        }
    }

    /**
     * Check if sound effects are enabled
     *
     * @return True if sound effects are enabled
     */
    public boolean isSoundOn() {
        return SoundStore.get().soundsOn();
    }

    /**
     * Indicate whether sound effects should be enabled
     *
     * @param on True if sound effects should be enabled
     */
    public void setSoundOn(boolean on) {
        SoundStore.get().setSoundsOn(on);
    }

    /**
     * Check if music is enabled
     *
     * @return True if music is enabled
     */
    public boolean isMusicOn() {
        return SoundStore.get().musicOn();
    }

    /**
     * Indicate whether music should be enabled
     *
     * @param on True if music should be enabled
     */
    public void setMusicOn(boolean on) {
        SoundStore.get().setMusicOn(on);
    }

    /**
     * Retrieve the current default volume for music
     *
     * @return the current default volume for music
     */
    public float getMusicVolume() {
        return SoundStore.get().getMusicVolume();
    }

    /**
     * Set the default volume for music
     *
     * @param volume the new default value for music volume
     */
    public void setMusicVolume(float volume) {
        SoundStore.get().setMusicVolume(volume);
    }

    /**
     * Retrieve the current default volume for sound fx
     *
     * @return the current default volume for sound fx
     */
    public float getSoundVolume() {
        return SoundStore.get().getSoundVolume();
    }

    /**
     * Set the default volume for sound fx
     *
     * @param volume the new default value for sound fx volume
     */
    public void setSoundVolume(float volume) {
        SoundStore.get().setSoundVolume(volume);
    }

    /**
     * Get the width of the standard screen resolution
     *
     * @return The screen width
     */
    @Override
    public abstract int getScreenWidth();

    /**
     * Get the height of the standard screen resolution
     *
     * @return The screen height
     */
    @Override
    public abstract int getScreenHeight();

    /**
     * Get the width of the game canvas
     *
     * @return The width of the game canvas
     */
    @Override
    public int getWidth() {
        return width;
    }

    /**
     * Get the height of the game canvas
     *
     * @return The height of the game canvas
     */
    @Override
    public int getHeight() {
        return height;
    }

    /**
     * Set the icon to be displayed if possible in this type of
     * container
     *
     * @param ref The reference to the icon to be displayed
     * @throws SlickException Indicates a failure to load the icon
     */
    public abstract void setIcon(String ref) throws SlickException;

    /**
     * Set the icons to be used for this application. Note that the size of the icon
     * defines how it will be used. Important ones to note
     * <p/>
     * Windows window icon must be 16x16
     * Windows alt-tab icon must be 24x24 or 32x32 depending on Windows version (XP=32)
     *
     * @param refs The reference to the icon to be displayed
     * @throws SlickException Indicates a failure to load the icon
     */
    public abstract void setIcons(String[] refs) throws SlickException;

    /**
     * Get the accurate system time
     *
     * @return The system time in milliseconds
     */
    @Override
    public long getTime() {
        return (Sys.getTime() * 1000) / Sys.getTimerResolution();
    }

    /**
     * Sleep for a given period
     *
     * @param milliseconds The period to sleep for in milliseconds
     */
    public void sleep(int milliseconds) {
        long target = getTime() + milliseconds;
        while (getTime() < target) {
            try {
                Thread.sleep(1);
            } catch (Exception e) {
            }
        }
    }

    /**
     * Set the mouse cursor to be displayed - this is a hardware cursor and hence
     * shouldn't have any impact on FPS.
     *
     * @param ref      The location of the image to be loaded for the cursor
     * @param hotSpotX The x coordinate of the hotspot within the cursor image
     * @param hotSpotY The y coordinate of the hotspot within the cursor image
     * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor
     */
    @Override
    public abstract void setMouseCursor(String ref, int hotSpotX, int hotSpotY)
        throws SlickException;

    /**
     * Set the mouse cursor to be displayed - this is a hardware cursor and hence
     * shouldn't have any impact on FPS.
     *
     * @param data     The image data from which the cursor can be construted
     * @param hotSpotX The x coordinate of the hotspot within the cursor image
     * @param hotSpotY The y coordinate of the hotspot within the cursor image
     * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor
     */
    @Override
    public abstract void setMouseCursor(ImageData data, int hotSpotX, int hotSpotY)
        throws SlickException;

    /**
     * Set the mouse cursor based on the contents of the image. Note that this will not take
     * account of render state type changes to images (rotation and such). If these effects
     * are required it is recommended that an offscreen buffer be used to produce an appropriate
     * image. An offscreen buffer will always be used to produce the new cursor and as such
     * this operation an be very expensive
     *
     * @param image    The image to use as the cursor
     * @param hotSpotX The x coordinate of the hotspot within the cursor image
     * @param hotSpotY The y coordinate of the hotspot within the cursor image
     * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor
     */
    public abstract void setMouseCursor(Image image, int hotSpotX, int hotSpotY)
        throws SlickException;

    /**
     * Set the mouse cursor to be displayed - this is a hardware cursor and hence
     * shouldn't have any impact on FPS.
     *
     * @param cursor   The cursor to use
     * @param hotSpotX The x coordinate of the hotspot within the cursor image
     * @param hotSpotY The y coordinate of the hotspot within the cursor image
     * @throws SlickException Indicates a failure to load the cursor image or create the hardware cursor
     */
    @Override
    public abstract void setMouseCursor(Cursor cursor, int hotSpotX, int hotSpotY)
        throws SlickException;

    /**
     * Get a cursor based on a image reference on the classpath. The image
     * is assumed to be a set/strip of cursor animation frames running from top to
     * bottom.
     *
     * @param ref          The reference to the image to be loaded
     * @param x            The x-coordinate of the cursor hotspot (left {@literal ->} right)
     * @param y            The y-coordinate of the cursor hotspot (bottom {@literal ->} top)
     * @param width        The x width of the cursor
     * @param height       The y height of the cursor
     * @param cursorDelays image delays between changing frames in animation
     * @throws SlickException Indicates a failure to load the image or a failure to create the hardware cursor
     */
    public void setAnimatedMouseCursor(String ref, int x, int y, int width, int height,
        int[] cursorDelays) throws SlickException {
        try {
            Cursor cursor;
            cursor = CursorLoader.get().getAnimatedCursor(ref, x, y, width, height, cursorDelays);
            setMouseCursor(cursor, x, y);
        } catch (IOException e) {
            throw new SlickException("Failed to set mouse cursor", e);
        } catch (LWJGLException e) {
            throw new SlickException("Failed to set mouse cursor", e);
        }
    }

    /**
     * Set the default mouse cursor - i.e. the original cursor before any native
     * cursor was set
     */
    @Override
    public abstract void setDefaultMouseCursor();

    /**
     * Get the input system
     *
     * @return The input system available to this game container
     */
    @Override
    public Input getInput() {
        return input;
    }

    /**
     * Get the current recorded FPS (frames per second)
     *
     * @return The current FPS
     */
    public int getFPS() {
        return recordedFPS;
    }

    /**
     * Check if the mouse cursor is current grabbed. This will cause it not
     * to be seen.
     *
     * @return True if the mouse is currently grabbed
     */
    public abstract boolean isMouseGrabbed();

    /**
     * Indicate whether mouse cursor should be grabbed or not
     *
     * @param grabbed True if mouse cursor should be grabbed
     */
    public abstract void setMouseGrabbed(boolean grabbed);

    /**
     * Retrieve the time taken to render the last frame, i.e. the change in time - delta.
     *
     * @return The time taken to render the last frame
     */
    protected int getDelta() {
        long time = getTime();
        int delta = (int) (time - lastFrame);
        lastFrame = time;

        return delta;
    }

    /**
     * Updated the FPS counter
     */
    protected void updateFPS() {
        if (getTime() - lastFPS > 1000) {
            lastFPS = getTime();
            recordedFPS = fps;
            fps = 0;
        }
        fps++;
    }

    /**
     * Set the minimum amount of time in milliseonds that has to
     * pass before update() is called on the container game. This gives
     * a way to limit logic updates compared to renders.
     *
     * @param interval The minimum interval between logic updates
     */
    public void setMinimumLogicUpdateInterval(int interval) {
        minimumLogicInterval = interval;
    }

    /**
     * Set the maximum amount of time in milliseconds that can passed
     * into the update method. Useful for collision detection without
     * sweeping.
     *
     * @param interval The maximum interval between logic updates
     */
    public void setMaximumLogicUpdateInterval(int interval) {
        maximumLogicInterval = interval;
    }

    /**
     * Update and render the game
     *
     * @param delta The change in time since last update and render
     * @throws SlickException Indicates an internal fault to the game.
     */
    protected void updateAndRender(int delta) throws SlickException {
        if (smoothDeltas) {
            if (getFPS() != 0) {
                delta = 1000 / getFPS();
            }
        }

        input.poll(width, height);

        Music.poll(delta);
        if (!paused) {
            storedDelta += delta;

            if (storedDelta >= minimumLogicInterval) {
                try {
                    if (maximumLogicInterval != 0) {
                        long cycles = storedDelta / maximumLogicInterval;
                        for (int i = 0; i < cycles; i++) {
                            game.update(this, (int) maximumLogicInterval);
                        }

                        int remainder = (int) (storedDelta % maximumLogicInterval);
                        if (remainder > minimumLogicInterval) {
                            game.update(this, (int) (remainder % maximumLogicInterval));
                            storedDelta = 0;
                        } else {
                            storedDelta = remainder;
                        }
                    } else {
                        game.update(this, (int) storedDelta);
                        storedDelta = 0;
                    }

                } catch (Throwable e) {
                    //					Log.error(e);
                    throw new SlickException("Game.update() failure.", e);
                }
            }
        } else {
            game.update(this, 0);
        }

        if (hasFocus() || getAlwaysRender()) {
            if (clearEachFrame) {
                GL.glClear(SGL.GL_COLOR_BUFFER_BIT | SGL.GL_DEPTH_BUFFER_BIT);
            }

            GL.glLoadIdentity();

            graphics.resetTransform();
            graphics.resetFont();
            graphics.resetLineWidth();
            graphics.setAntiAlias(false);
            try {
                game.render(this, graphics);
            } catch (Throwable e) {
                //				Log.error(e);
                throw new SlickException("Game.render() failure.", e);
            }
            graphics.resetTransform();

            if (showFPS) {
                defaultFont.drawString(10, 10, "FPS: " + recordedFPS);
            }

            GL.flush();
        }

        if (targetFPS != -1) {
            Display.sync(targetFPS);
        }
    }

    /**
     * Indicate if the display should update only when the game is visible
     * (the default is true)
     *
     * @param updateOnlyWhenVisible True if we should updated only when the display is visible
     */
    public void setUpdateOnlyWhenVisible(boolean updateOnlyWhenVisible) {
    }

    /**
     * Check if this game is only updating when visible to the user (default = true)
     *
     * @return True if the game is only updated when the display is visible
     */
    public boolean isUpdatingOnlyWhenVisible() {
        return true;
    }

    /**
     * Initialise the GL context
     */
    protected void initGL() {
        Log.info("Starting display " + width + "x" + height);
        GL.initDisplay(width, height);

        if (input == null) {
            input = new Input(height);
        }
        input.init(height);
        // no need to remove listeners?
        //input.removeAllListeners();
        if (game instanceof InputListener) {
            input.removeListener((InputListener) game);
            input.addListener((InputListener) game);
        }

        if (graphics != null) {
            graphics.setDimensions(getWidth(), getHeight());
        }
        lastGame = game;
    }

    /**
     * Initialise the system components, OpenGL and OpenAL.
     *
     * @throws SlickException Indicates a failure to create a native handler
     */
    protected void initSystem() throws SlickException {
        initGL();
        setMusicVolume(1.0f);
        setSoundVolume(1.0f);

        graphics = new Graphics(width, height);
        defaultFont = graphics.getFont();
    }

    /**
     * Enter the orthographic mode
     */
    protected void enterOrtho() {
        enterOrtho(width, height);
    }

    /**
     * Indicate whether the container should show the FPS
     *
     * @param show True if the container should show the FPS
     */
    public void setShowFPS(boolean show) {
        showFPS = show;
    }

    /**
     * Check if the FPS is currently showing
     *
     * @return True if the FPS is showing
     */
    public boolean isShowingFPS() {
        return showFPS;
    }

    /**
     * Set the target fps we're hoping to get
     *
     * @param fps The target fps we're hoping to get
     */
    public void setTargetFrameRate(int fps) {
        targetFPS = fps;
    }

    /**
     * Indicate whether the display should be synced to the
     * vertical refresh (stops tearing)
     *
     * @param vsync True if we want to sync to vertical refresh
     */
    public void setVSync(boolean vsync) {
        this.vsync = vsync;
        Display.setVSyncEnabled(vsync);
    }

    /**
     * True if vsync is requested
     *
     * @return True if vsync is requested
     */
    public boolean isVSyncRequested() {
        return vsync;
    }

    /**
     * True if the game is running
     *
     * @return True if the game is running
     */
    protected boolean running() {
        return running;
    }

    /**
     * Inidcate we want verbose logging
     *
     * @param verbose True if we want verbose logging (INFO and DEBUG)
     */
    public void setVerbose(boolean verbose) {
        Log.setVerbose(verbose);
    }

    /**
     * Cause the game to exit and shutdown cleanly
     */
    public void exit() {
        running = false;
    }

    /**
     * Check if the game currently has focus
     *
     * @return True if the game currently has focus
     */
    public abstract boolean hasFocus();

    /**
     * Get the graphics context used by this container. Note that this
     * value may vary over the life time of the game.
     *
     * @return The graphics context used by this container
     */
    public Graphics getGraphics() {
        return graphics;
    }

    /**
     * Enter the orthographic mode
     *
     * @param xsize The size of the panel being used
     * @param ysize The size of the panel being used
     */
    protected void enterOrtho(int xsize, int ysize) {
        GL.enterOrtho(xsize, ysize);
    }
}
